(function (global) {
    var re = {
        starts_with_slashes: /^\/+/,
        ends_with_slashes: /\/+$/,
        pluses: /\+/g,
        query_separator: /[&;]/,
        uri_parser: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*)(?::([^:@]*))?)?@)?(\[[0-9a-fA-F:.]+\]|[^:\/?#]*)(?::(\d+|(?=:)))?(:)?)((((?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
    };
    if (!Array.prototype.forEach) {
        Array.prototype.forEach = function (callback, thisArg) {
            var T, k;
            if (this == null) {
                throw new TypeError(' this is null or not defined');
            }
            var O = Object(this);
            var len = O.length >>> 0;
            if (typeof callback !== "function") {
                throw new TypeError(callback + ' is not a function');
            }
            if (arguments.length > 1) {
                T = thisArg
            }
            k = 0;
            while (k < len) {
                var kValue;
                if (k in O) {
                    kValue = O[k];
                    callback.call(T, kValue, k, O)
                }
                k++
            }
        }
    }

    function decode(s) {
        if (s) {
            s = s.toString().replace(re.pluses, '%20');
            s = decodeURIComponent(s)
        }
        return s
    }

    function parseUri(str) {
        var parser = re.uri_parser;
        var parserKeys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "isColonUri", "relative", "path", "directory", "file", "query", "anchor"];
        var m = parser.exec(str || '');
        var parts = {};
        parserKeys.forEach(function (key, i) {
            parts[key] = m[i] || ''
        });
        return parts
    }

    function parseQuery(str) {
        var i, ps, p, n, k, v, l;
        var pairs = [];
        if (typeof (str) === 'undefined' || str === null || str === '') {
            return pairs
        }
        if (str.indexOf('?') === 0) {
            str = str.substring(1)
        }
        ps = str.toString().split(re.query_separator);
        for (i = 0, l = ps.length; i < l; i++) {
            p = ps[i];
            n = p.indexOf('=');
            if (n !== 0) {
                k = decode(p.substring(0, n));
                v = decode(p.substring(n + 1));
                pairs.push(n === -1 ? [p, null] : [k, v])
            }
        }
        return pairs
    }

    function Uri(str) {
        this.uriParts = parseUri(str);
        this.queryPairs = parseQuery(this.uriParts.query);
        this.hasAuthorityPrefixUserPref = null
    }

    ['protocol', 'userInfo', 'host', 'port', 'path', 'anchor'].forEach(function (key) {
        Uri.prototype[key] = function (val) {
            if (typeof val !== 'undefined') {
                this.uriParts[key] = val
            }
            return this.uriParts[key]
        }
    });
    Uri.prototype.hasAuthorityPrefix = function (val) {
        if (typeof val !== 'undefined') {
            this.hasAuthorityPrefixUserPref = val
        }
        if (this.hasAuthorityPrefixUserPref === null) {
            return (this.uriParts.source.indexOf('//') !== -1)
        } else {
            return this.hasAuthorityPrefixUserPref
        }
    };
    Uri.prototype.isColonUri = function (val) {
        if (typeof val !== 'undefined') {
            this.uriParts.isColonUri = !!val
        } else {
            return !!this.uriParts.isColonUri
        }
    };
    Uri.prototype.query = function (val) {
        var s = '', i, param, l;
        if (typeof val !== 'undefined') {
            this.queryPairs = parseQuery(val)
        }
        for (i = 0, l = this.queryPairs.length; i < l; i++) {
            param = this.queryPairs[i];
            if (s.length > 0) {
                s += '&'
            }
            if (param[1] === null) {
                s += param[0]
            } else {
                s += param[0];
                s += '=';
                if (typeof param[1] !== 'undefined') {
                    s += encodeURIComponent(param[1])
                }
            }
        }
        return s.length > 0 ? '?' + s : s
    };
    Uri.prototype.getQueryParamValue = function (key) {
        var param, i, l;
        for (i = 0, l = this.queryPairs.length; i < l; i++) {
            param = this.queryPairs[i];
            if (key === param[0]) {
                return param[1]
            }
        }
    };
    Uri.prototype.getQueryParamValues = function (key) {
        var arr = [], i, param, l;
        for (i = 0, l = this.queryPairs.length; i < l; i++) {
            param = this.queryPairs[i];
            if (key === param[0]) {
                arr.push(param[1])
            }
        }
        return arr
    };
    Uri.prototype.deleteQueryParam = function (key, val) {
        var arr = [], i, param, keyMatchesFilter, valMatchesFilter, l;
        for (i = 0, l = this.queryPairs.length; i < l; i++) {
            param = this.queryPairs[i];
            keyMatchesFilter = decode(param[0]) === decode(key);
            valMatchesFilter = param[1] === val;
            if ((arguments.length === 1 && !keyMatchesFilter) || (arguments.length === 2 && (!keyMatchesFilter || !valMatchesFilter))) {
                arr.push(param)
            }
        }
        this.queryPairs = arr;
        return this
    };
    Uri.prototype.addQueryParam = function (key, val, index) {
        if (arguments.length === 3 && index !== -1) {
            index = Math.min(index, this.queryPairs.length);
            this.queryPairs.splice(index, 0, [key, val])
        } else if (arguments.length > 0) {
            this.queryPairs.push([key, val])
        }
        return this
    };
    Uri.prototype.hasQueryParam = function (key) {
        var i, len = this.queryPairs.length;
        for (i = 0; i < len; i++) {
            if (this.queryPairs[i][0] == key) return true
        }
        return false
    };
    Uri.prototype.replaceQueryParam = function (key, newVal, oldVal) {
        var index = -1, len = this.queryPairs.length, i, param;
        if (arguments.length === 3) {
            for (i = 0; i < len; i++) {
                param = this.queryPairs[i];
                if (decode(param[0]) === decode(key) && decodeURIComponent(param[1]) === decode(oldVal)) {
                    index = i;
                    break
                }
            }
            if (index >= 0) {
                this.deleteQueryParam(key, decode(oldVal)).addQueryParam(key, newVal, index)
            }
        } else {
            for (i = 0; i < len; i++) {
                param = this.queryPairs[i];
                if (decode(param[0]) === decode(key)) {
                    index = i;
                    break
                }
            }
            this.deleteQueryParam(key);
            this.addQueryParam(key, newVal, index)
        }
        return this
    };
    ['protocol', 'hasAuthorityPrefix', 'isColonUri', 'userInfo', 'host', 'port', 'path', 'query', 'anchor'].forEach(function (key) {
        var method = 'set' + key.charAt(0).toUpperCase() + key.slice(1);
        Uri.prototype[method] = function (val) {
            this[key](val);
            return this
        }
    });
    Uri.prototype.scheme = function () {
        var s = '';
        if (this.protocol()) {
            s += this.protocol();
            if (this.protocol().indexOf(':') !== this.protocol().length - 1) {
                s += ':'
            }
            s += '//'
        } else {
            if (this.hasAuthorityPrefix() && this.host()) {
                s += '//'
            }
        }
        return s
    };
    Uri.prototype.origin = function () {
        var s = this.scheme();
        if (this.userInfo() && this.host()) {
            s += this.userInfo();
            if (this.userInfo().indexOf('@') !== this.userInfo().length - 1) {
                s += '@'
            }
        }
        if (this.host()) {
            s += this.host();
            if (this.port() || (this.path() && this.path().substr(0, 1).match(/[0-9]/))) {
                s += ':' + this.port()
            }
        }
        return s
    };
    Uri.prototype.addTrailingSlash = function () {
        var path = this.path() || '';
        if (path.substr(-1) !== '/') {
            this.path(path + '/')
        }
        return this
    };
    Uri.prototype.toString = function () {
        var path, s = this.origin();
        if (this.isColonUri()) {
            if (this.path()) {
                s += ':' + this.path()
            }
        } else if (this.path()) {
            path = this.path();
            if (!(re.ends_with_slashes.test(s) || re.starts_with_slashes.test(path))) {
                s += '/'
            } else {
                if (s) {
                    s.replace(re.ends_with_slashes, '/')
                }
                path = path.replace(re.starts_with_slashes, '/')
            }
            s += path
        } else {
            if (this.host() && (this.query().toString() || this.anchor())) {
                s += '/'
            }
        }
        if (this.query().toString()) {
            s += this.query().toString()
        }
        if (this.anchor()) {
            if (this.anchor().indexOf('#') !== 0) {
                s += '#'
            }
            s += this.anchor()
        }
        return s
    };
    Uri.prototype.clone = function () {
        return new Uri(this.toString())
    };
    if (typeof define === 'function' && define.amd) {
        define(function () {
            return Uri
        })
    } else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
        module.exports = Uri
    } else {
        globalThis.Uri = Uri
    }
}(this));